﻿using UnityEngine;
using UnityEngine.VR;
using UnityEngine.Rendering;

using System.Collections;
using System.Collections.Generic;
using System.Linq;

#if UNITY_EDITOR
using UnityEditor;
#endif

namespace PluginIdea
{
    /**
    * \mainpage Welcome to the documentation
    * To keep things from being too overwhelming I've only documented what's necessary to script with the system.
    * For more advanced users, everything in the system is commented and built to be expanded on. Dig through the code to your hearts content.
    * If you have any questions or get stuck at any stage I'm always available at Support@LlockhamIndustries.com.
    */

    /**
    * The core class of the system, responsible for the majority of the systems functionality. 
    * For scripting purposes, it's almost entirely a black box, you should rarely need to access or modify anything within it.
    * It's well stuctured and commented all the same though, so if your interested, open it up and have a look around.
    */
    [ExecuteInEditMode]
    public class DynamicDecals : MonoBehaviour
    {
        public EasyDecalManager mgr;

        //MultiScene Editor Singleton
        public static bool Initialized
        {
            get { return system != null; }
        }
        public static DynamicDecals System
        {
            get
            {
                if (system == null)
                {
                    //Create hidden host game object
                    GameObject go = new GameObject("Dynamic Decals");
                    go.hideFlags = HideFlags.DontSaveInEditor | HideFlags.DontSaveInBuild | HideFlags.HideInHierarchy | HideFlags.HideInInspector;

                    //Setup system
                    go.AddComponent<DynamicDecals>();
                }
                return system;
            }
        }
        private static DynamicDecals system;

        private void Start()
        {
            //Don't destroy on scene switch
            if (Application.isPlaying) DontDestroyOnLoad(gameObject);
        }
        private void OnEnable()
        {
            //Singleton
            if (system == null)
            {
                system = this;
            }
            else if (system != this)
            {
                if (Application.isPlaying) Destroy(gameObject);
                else DestroyImmediate(gameObject, true);
                return;
            }

            Initialize();
        }
        private void OnDisable()
        {
            //Terminate the system
            Terminate();
        }

#if UNITY_EDITOR
        private void OnApplicationQuit()
        {
            //Reset the system when transitioning back to edit mode
            Terminate();
            Initialize();
        }
#endif

        private bool fireInCulling = true;

        #region Rendering
        //0: forward  1:deffered
        public int renderingPath;

        //Shader Replacement
        public bool ShaderReplacement
        {
            get { return shaderReplacement; }
            set { shaderReplacement = value; }
        }
        private bool shaderReplacement = true;

        internal RenderTextureFormat depthFormat;
        internal RenderTextureFormat normalFormat;
        internal RenderTextureFormat maskFormat;

        //Instanced
        public bool Instanced
        {
            get { return SystemInfo.supportsInstancing; }
        }

        //Methods
        private void UpdateSystemPath()
        {
            //Get our primary camera
            Camera target = null;
            if (Camera.main != null) target = Camera.main;
            else if (Camera.current != null) target = Camera.current;

            if (target != null)
            {
                //Determine our rendering method
                if (target.actualRenderingPath == RenderingPath.Forward || target.actualRenderingPath == RenderingPath.DeferredShading)
                {
                    int newPath = 0;
                    if (target.actualRenderingPath == RenderingPath.DeferredShading) newPath = 1;

                    if (renderingPath != newPath)
                    {
                        renderingPath = newPath;
                        // UpdateRenderers();
                    }
                }
                else Debug.LogWarning("Current Rendering Path not supported! Please use either Forward or Deferred");
            }
        }
        public void RestoreDepthTextureModes()
        {
            //Iterate over every camera and restore it to it's original depth texture mode
            for (int i = 0; i < cameraData.Count; i++)
            {
                Camera camera = cameraData.ElementAt(i).Key;
                if (camera != null) cameraData.ElementAt(i).Value.RestoreDepthTextureMode(camera);
            }
        }
        #endregion
        public void MarkRenderers()
        {
            renderersMarked = true;
        }
        private bool renderersMarked;

        #region Meshes & Shaders

        public Shader DepthShader
        {
            get
            {
                if (depthShader == null)
                {
                    //depthShader = Shader.Find("Projection/Internal/Depth");
                    depthShader = mgr.dic_BundleShaders["Projection/Internal/Depth"];
                }
                return depthShader;
            }

        }
        public Shader NormalShader
        {
            get
            {
                if (normalShader == null)
                {
                    //normalShader = Shader.Find("Projection/Internal/Normal");
                    normalShader = mgr.dic_BundleShaders["Projection/Internal/Normal"]; 
                }
                return normalShader;
            }
        }
        public Shader MaskShader
        {
            get
            {
                if (maskShader == null)
                {
                    //maskShader = Shader.Find("Projection/Internal/Mask");
                    maskShader = mgr.dic_BundleShaders["Projection/Internal/Mask"];
                }
                return maskShader;
            }
        }

        public Shader DepthNormalShader
        {
            get
            {
                if (depthNormalShader == null)
                {
                    //depthNormalShader = Shader.Find("Projection/Internal/DepthNormal");
                    depthNormalShader = mgr.dic_BundleShaders["Projection/Internal/DepthNormal"];
                }
                return depthNormalShader;
            }
        }
        public Shader NormalMaskShader
        {
            get
            {
                if (normalMaskShader == null)
                {
                    //normalMaskShader = Shader.Find("Projection/Internal/NormalMask");
                    normalMaskShader = mgr.dic_BundleShaders["Projection/Internal/NormalMask"];
                }
                return normalMaskShader;
            }
        }

        public Shader DepthNormalMaskShader
        {
            get
            {
                if (depthNormalMaskShader == null)
                {
                    //depthNormalMaskShader = Shader.Find("Projection/Internal/DepthNormalMask");
                    depthNormalMaskShader = mgr.dic_BundleShaders["Projection/Internal/DepthNormalMask"];
                }
                return depthNormalMaskShader;
            }
        }
        public Shader DepthNormalMaskShader_Packed
        {
            get
            {
                if (depthNormalMaskShader_Packed == null)
                {
                    //depthNormalMaskShader_Packed = Shader.Find("Projection/Internal/DepthNormalMask_Packed");
                    depthNormalMaskShader_Packed = mgr.dic_BundleShaders["Projection/Internal/DepthNormalMask_Packed"];
                }
                return depthNormalMaskShader_Packed;
            }
        }

        public Material StereoBlitLeft
        {
            get
            {
                if (stereoBlitLeft == null)
                {
                    //stereoBlitLeft = new Material(Shader.Find("Projection/Internal/StereoBlitLeft"));
                    stereoBlitLeft = new Material(mgr.dic_BundleShaders["Projection/Internal/StereoBlitLeft"]);
                }
                return stereoBlitLeft;
            }
        }
        public Material StereoBlitRight
        {
            get
            {
                if (stereoBlitRight == null)
                {
                    //stereoBlitRight = new Material(Shader.Find("Projection/Internal/StereoBlitRight"));
                    stereoBlitRight = new Material(mgr.dic_BundleShaders["Projection/Internal/StereoBlitRight"]);
                }
                return stereoBlitRight;
            }
        }
        public Material StereoDepthBlitLeft
        {
            get
            {
                if (stereoDepthBlitLeft == null)
                {
                    //stereoDepthBlitLeft = new Material(Shader.Find("Projection/Internal/StereoDepthBlitLeft"));
                    stereoDepthBlitLeft = new Material(mgr.dic_BundleShaders["Projection/Internal/StereoDepthBlitLeft"]);
                }
                return stereoDepthBlitLeft;
            }
        }
        public Material StereoDepthBlitRight
        {
            get
            {
                if (stereoDepthBlitRight == null)
                {
                    //stereoDepthBlitRight = new Material(Shader.Find("Projection/Internal/StereoDepthBlitRight"));
                    stereoDepthBlitRight = new Material(mgr.dic_BundleShaders["Projection/Internal/StereoDepthBlitRight"]);
                }
                return stereoDepthBlitRight;
            }
        }

        //Backing Fields
        private Mesh cube;

        private Shader depthShader;
        private Shader normalShader;
        private Shader maskShader;

        private Shader depthNormalShader;
        private Shader normalMaskShader;

        private Shader depthNormalMaskShader;
        private Shader depthNormalMaskShader_Packed;

        private Material stereoBlitLeft;
        private Material stereoBlitRight;
        private Material stereoDepthBlitLeft;
        private Material stereoDepthBlitRight;
        #endregion
        #region Cameras
        //Scene camera data
        internal Dictionary<Camera, CameraData> cameraData = new Dictionary<Camera, CameraData>();
        internal CameraData GetData(Camera Camera)
        {
            //Declare our Camera Data
            CameraData data = null;

            //Check if this camera already has camera data
            if (!cameraData.TryGetValue(Camera, out data))
            {
                //Generate data
                data = new CameraData();

                //Store data
                cameraData[Camera] = data;
            }

            //Initialize if required
            if (data != null)
            {
                if (!data.initialized && Camera.GetComponent<ProjectionBlocker>() == null) data.Initialize(Camera, this);
                else if (data.initialized && Camera.GetComponent<ProjectionBlocker>() != null) data.Terminate(Camera);
            }

            //Return our updated Camera Data
            return data;
        }

        //Shader replacement camera
        public Camera CustomCamera
        {
            get
            {
                if (customCamera == null)
                {
                    GameObject cameraObject = new GameObject("Custom Camera");
                    customCamera = cameraObject.AddComponent<Camera>();
                    cameraObject.AddComponent<ProjectionBlocker>();
                    cameraObject.hideFlags = HideFlags.DontSaveInEditor | HideFlags.DontSaveInBuild | HideFlags.HideInHierarchy | HideFlags.HideInInspector;
                    cameraObject.SetActive(false);

                    if (Application.isPlaying) GameObject.DontDestroyOnLoad(cameraObject);
                }
                return customCamera;
            }
        }
        private Camera customCamera;
        #endregion

        public static bool isSinglePassVR = false;

        //Initialize / Terminate
        private void Initialize()
        {
#if UNITY_EDITOR
            // Settings.CalculateVR();

            // SceneView.onSceneGUIDelegate += OnSceneGUI;
            Undo.undoRedoPerformed += UndoRedo;
#endif

            //Determine texture formats
            depthFormat = RenderTextureFormat.Depth;
            normalFormat = SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.ARGB2101010) ? RenderTextureFormat.ARGB2101010 : RenderTextureFormat.ARGB32;
            maskFormat = SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.ARGB32) ? RenderTextureFormat.ARGB32 : RenderTextureFormat.ARGB32;

            //Register our projection events to all cameras
            Camera.onPreCull += SuperLateUpdate;
            Camera.onPreRender += PreRender;
        }
        private void Terminate()
        {
#if UNITY_EDITOR
            // SceneView.onSceneGUIDelegate -= OnSceneGUI;
            Undo.undoRedoPerformed -= UndoRedo;
#endif

            //Deregister our projection events
            Camera.onPreCull -= SuperLateUpdate;
            Camera.onPreRender -= PreRender;

            //Iterate over our camera data
            foreach (var cb in cameraData)
            {
                //Terminate camera data
                cb.Value.Terminate(cb.Key);
            }

            //Clear camera Data
            cameraData.Clear();
        }

        //Primary Methods
        private void LateUpdate()
        {
            //Check our system path
            UpdateSystemPath();

            //Uncomment below to log debug information while in a development build
            //DebugInDevelopmentBuild();
        }
        private void SuperLateUpdate(Camera Camera)
        {
            if (fireInCulling)
            {
                //Grab our camera data
                CameraData data = GetData(Camera);

                //Only run on initialized cameras
                if (data.initialized && (Camera.cameraType == CameraType.SceneView || Camera.cameraType == CameraType.Preview || Camera.isActiveAndEnabled))
                {
                    //Update and perform shader replacement
                    data.Update(Camera, this);
                }
            }
        }
        private void PreRender(Camera Camera)
        {
            //Grab our camera data
            CameraData data = GetData(Camera);

            //Only run on initialized cameras
            if (data.initialized && (Camera.cameraType == CameraType.SceneView || Camera.cameraType == CameraType.Preview || Camera.isActiveAndEnabled))
            {
                if (!fireInCulling)
                {
                    //Update and perform shader replacement
                    data.Update(Camera, this);
                }

                //Shader replacement
                data.AssignGlobalProperties(Camera);
            }
        }

        //Debug
        public static string DebugLog
        {
            get
            {
                string debug = "Debug Information (Copy and Paste) \r\n";

                //General settings
                debug += "\r\nGeneral\r\n";
                debug += "OS : " + SystemInfo.operatingSystem + "\r\n";
                debug += "Graphics device : " + SystemInfo.graphicsDeviceName + "\r\n";
                debug += "Graphics API : " + SystemInfo.graphicsDeviceType + "\r\n";

                //Camera & shader replacement settings
                Camera cam = Camera.main;
                if (cam != null)
                {
                    debug += "\r\nCamera\r\n";
                    debug += "Rendering path : " + cam.actualRenderingPath + "\r\n";
                    debug += "Is orthographic : " + cam.orthographic + "\r\n";

                    debug += "\r\nShader Replacement\r\n";
                    debug += "Method : " + System.GetData(cam).replacement.ToString() + "\r\n";
                }
                else
                {
                    debug += "\r\nMain camera not found\r\nPlease tag your main camera\r\n";
                }

                //Virtual reality settings
                if (UnityEngine.XR.XRSettings.enabled)
                {
                    debug += "\r\nVirtualReality : " + UnityEngine.XR.XRSettings.isDeviceActive + "\r\n";
                    debug += "VR API : " + UnityEngine.XR.XRSettings.loadedDeviceName + "\r\n";
                    debug += "VR device : " + UnityEngine.XR.XRDevice.model + "\r\n";

#if UNITY_EDITOR
                    // debug += "Stereo rendering path : " + (System.Settings.SinglePassVR? "SinglePass" : "MultiPass") + "\r\n";
                    debug += "Stereo rendering path : " + (isSinglePassVR ? "SinglePass" : "MultiPass") + "\r\n";
#endif
                }

                return debug;
            }
        }
        public static void DebugInDevelopmentBuild()
        {
            if (Debug.isDebugBuild) Debug.Log(DebugLog);
        }

        //Editor Scene Placement
#if UNITY_EDITOR
        private List<GameObject> dragables = new List<GameObject>();
        private void OnSceneGUI(SceneView sceneView)
        {
            //Drag Update
            if (Event.current.type == EventType.DragUpdated)
            {
                //Check for projection renderers among selection
                if (dragables.Count == 0)
                {
                    foreach (UnityEngine.Object o in DragAndDrop.objectReferences)
                    {
                        GameObject go = o as GameObject;
                        if (go != null)
                        {
                            // if (go.GetComponent<ProjectionRenderer>() != null)
                            // {
                            //     //Create our dragable
                            //     GameObject dragable = PrefabUtility.InstantiatePrefab(go) as GameObject;
                            //     dragable.name = go.name;
                            //     dragable.hideFlags = HideFlags.HideInHierarchy;

                            //     //Register to list
                            //     dragables.Add(dragable);
                            // }
                        }
                    }
                }

                //Position dragables
                if (dragables.Count > 0)
                {
                    RaycastHit hit;
                    Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);

                    if (Physics.Raycast(ray, out hit, Mathf.Infinity))
                    {
                        foreach (GameObject go in dragables)
                        {
                            go.transform.position = hit.point;
                            go.transform.rotation = Quaternion.LookRotation(-hit.normal);
                        }
                    }
                    else
                    {
                        foreach (GameObject go in dragables)
                        {
                            go.transform.position = Vector3.zero;
                            go.transform.rotation = Quaternion.LookRotation(-Vector3.up);
                        }
                    }

                    //Set mode
                    DragAndDrop.visualMode = DragAndDropVisualMode.Copy;

                    //Use event
                    Event.current.Use();
                }
            }

            //Drag Perform
            if (Event.current.type == EventType.DragPerform && dragables.Count > 0)
            {
                //Show dragables
                foreach (GameObject go in dragables)
                {
                    //Show objects in heirarchy
                    go.hideFlags = HideFlags.None;

                    //Register Undo
                    Undo.RegisterCreatedObjectUndo(go, "Instantiate Prefab");
                }

                //No longer require dragables
                dragables.Clear();

                //Use event
                Event.current.Use();
            }

            //Drag Exit
            if (Event.current.type == EventType.DragExited && dragables.Count > 0)
            {
                //Destroy all dragables
                foreach (GameObject go in dragables) DestroyImmediate(go);
                dragables.Clear();

                //Use event
                Event.current.Use();
            }
        }
        private void UndoRedo()
        {
            dragables.Clear();
        }
#endif
    }

    //System path
    //public enum SystemPath { Forward, Deferred };

    //Camera management
    internal class CameraData
    {
        //Depth texture mode
        public DepthTextureMode? originalDTM, desiredDTM = null;

        //Shader replacement
        /// <summary>
        /// Null:1, SingleTarget:2, SinglePass:3, DoublePass:4, TriplePass:5, Classic:6 }
        /// </summary>
        public int replacement;

        //Render textures
        private RenderTexture depthBuffer;
        private RenderTexture normalBuffer;
        private RenderTexture maskBuffer;

        //Eye textures (Single pass stereo)
        private RenderTexture depthEye;
        private RenderTexture normalEye;
        private RenderTexture maskEye;

        //Core functions
        public bool initialized;
        public void Initialize(Camera Camera, DynamicDecals System)
        {
            initialized = true;
        }
        public void Terminate(Camera Camera)
        {
            //Restore cameras depthTexture mode
            RestoreDepthTextureMode(Camera);

            //Release render textures
            ReleaseTextures();

            //Disable
            initialized = false;
        }
        public void Update(Camera Camera, DynamicDecals System)
        {
            //Update rendering method
            UpdateRenderingMethod(Camera, System);

            //Update render textures
            UpdateRenderTextures(Camera, System);

            //render shader replacement
            UpdateShaderReplacement(Camera, System);
        }

        //PreRender
        public void AssignGlobalProperties(Camera Camera)
        {
            if (replacement != 0)
            {
                switch (replacement)
                {
                    case 5:
                        //Assign mask buffer
                        maskBuffer.SetGlobalShaderProperty("_MaskBuffer_0");

                        //Tell shaders not to use custom or precision depth/normals
                        Shader.DisableKeyword("_PrecisionDepthNormals");
                        Shader.DisableKeyword("_CustomDepthNormals");
                        Shader.DisableKeyword("_PackedDepthNormals");
                        break;

                    case 4:
                    case 3:
                        //Assign custom buffers
                        if (Camera.actualRenderingPath == RenderingPath.DeferredShading)
                        {
                            //Assign custom depth/normals
                            depthBuffer.SetGlobalShaderProperty("_CustomDepthTexture");
                            normalBuffer.SetGlobalShaderProperty("_CustomNormalTexture");

                            //Tell shaders to use custom depth/normals
                            Shader.DisableKeyword("_PrecisionDepthNormals");
                            Shader.EnableKeyword("_CustomDepthNormals");
                            Shader.DisableKeyword("_PackedDepthNormals");
                        }
                        else
                        {
                            //Assign custom normals
                            normalBuffer.SetGlobalShaderProperty("_CustomNormalTexture");

                            //Tell shaders to use precision depth/normals
                            Shader.EnableKeyword("_PrecisionDepthNormals");
                            Shader.DisableKeyword("_CustomDepthNormals");
                            Shader.DisableKeyword("_PackedDepthNormals");
                        }

                        //Assign mask buffer
                        maskBuffer.SetGlobalShaderProperty("_MaskBuffer_0");
                        break;

                    case 2:
                        //Assign custom depth/normals/mask
                        depthBuffer.SetGlobalShaderProperty("_CustomDepthTexture");
                        normalBuffer.SetGlobalShaderProperty("_CustomNormalTexture");
                        maskBuffer.SetGlobalShaderProperty("_MaskBuffer_0");

                        //Tell shaders to use custom depth/normals
                        Shader.DisableKeyword("_PrecisionDepthNormals");
                        Shader.EnableKeyword("_CustomDepthNormals");
                        Shader.DisableKeyword("_PackedDepthNormals");
                        break;

                    case 1:
                        //Assign custom depth/normals
                        depthBuffer.SetGlobalShaderProperty("_CustomDepthNormalMaskTexture");

                        //Tell shaders to use packed depth normals
                        Shader.DisableKeyword("_PrecisionDepthNormals");
                        Shader.DisableKeyword("_CustomDepthNormals");
                        Shader.EnableKeyword("_PackedDepthNormals");
                        break;
                }


            }
        }

        //Rendering method
        private int Standard
        {
            get
            {
#if !UNITY_WEBGL
                //Use single target where possible
                if (SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.RGFloat) && SystemInfo.graphicsShaderLevel > 30)
                {
                    return 1;
                }
#endif

                //Singlepass requires rendering to 3 buffers
                if (SystemInfo.supportedRenderTargetCount < 3)
                {
                    return 3;
                }

                //DirectX9 Doesn't seem to like rendering to different renderTexture formats at the same time
                if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Direct3D9)
                {
                    return 3;
                }

                //Otherwise use singlepass
                return 2;
            }
        }
        private int VR
        {
            get
            {
                //Use single target where possible
                if (SystemInfo.SupportsRenderTextureFormat(RenderTextureFormat.RGFloat) && SystemInfo.graphicsShaderLevel > 30)
                {
                    return 1;
                }
                else return 4;
            }
        }
        private bool VRCamera(Camera Source)
        {
            if (Source.cameraType == CameraType.SceneView || Source.cameraType == CameraType.Preview) return false;
            return (UnityEngine.XR.XRSettings.enabled && Source.stereoTargetEye != StereoTargetEyeMask.None);
        }

        private void UpdateRenderingMethod(Camera Camera, DynamicDecals System)
        {
            //Use standard shader replacement by default
            int shaderReplacement = Standard;

            //If rendering VR, use TriplePass replacement
            if (UnityEngine.XR.XRSettings.enabled && Camera.stereoTargetEye != StereoTargetEyeMask.None) shaderReplacement = VR;

            //If on old device, use classic replacement
            if (Camera.cameraType != CameraType.SceneView && Camera.cameraType != CameraType.Preview && (SystemInfo.graphicsShaderLevel < 30 || SystemInfo.supportedRenderTargetCount < 2)) shaderReplacement = 5;

            //Determine our rendering method
            if (replacement != shaderReplacement)
            {
                //Update our rendering method
                replacement = shaderReplacement;

                //Add ourself to the new rendering method
                SwitchRenderingMethod(Camera);
            }
        }
        private void SwitchRenderingMethod(Camera Camera)
        {
            switch (replacement)
            {
                case 5:
                    //Low precision depth & normals
                    desiredDTM = DepthTextureMode.DepthNormals;
                    SetDepthTextureMode(Camera);
                    break;

                case 4:
                case 3:

                    if (Camera.actualRenderingPath == RenderingPath.DeferredShading)
                    {
                        //Use custom depth, Unity's depth renders too late
                        RestoreDepthTextureMode(Camera);
                    }
                    else
                    {
                        //High precision depth
                        desiredDTM = DepthTextureMode.Depth;
                        SetDepthTextureMode(Camera);
                    }

                    break;

                case 2:
                case 1:
                    //Restore our depth texture mode
                    RestoreDepthTextureMode(Camera);
                    break;
            }
        }

        //Native shader replacement
        private void SetDepthTextureMode(Camera Camera)
        {
            //If we have a desired value change to it.
            if (desiredDTM.HasValue)
            {
                if (Camera.depthTextureMode != desiredDTM)
                {
                    //If we haven't already, Cache the original depth texture mode, otherwise revert to it.
                    if (!originalDTM.HasValue) originalDTM = Camera.depthTextureMode;
                    else Camera.depthTextureMode = originalDTM.Value;

                    //Add our desired depth texture mode.
                    Camera.depthTextureMode |= desiredDTM.Value;
                }
            }
            //If we have no desired value, switch back to the original value.
            else RestoreDepthTextureMode(Camera);
        }
        public void RestoreDepthTextureMode(Camera Camera)
        {
            //Restore the depth texture mode to the cached
            if (originalDTM.HasValue && Camera != null)
            {
                Camera.depthTextureMode = originalDTM.Value;
            }
        }

        //Custom render textures
        private void UpdateRenderTextures(Camera Camera, DynamicDecals System)
        {
            //Calculate width and height
            int width = Camera.pixelWidth;
            int height = Camera.pixelHeight;

            //VR cameras use eye texture values
            if (VRCamera(Camera))
            {
                width = (DynamicDecals.isSinglePassVR) ? UnityEngine.XR.XRSettings.eyeTextureWidth * 2 : UnityEngine.XR.XRSettings.eyeTextureWidth;
                height = UnityEngine.XR.XRSettings.eyeTextureHeight;
            }

            //If the size has changed, grab new render textures of the new size
            if (maskBuffer == null || maskBuffer.width != width || maskBuffer.height != height)
            {
                ReleaseTextures();
                GetTextures(Camera, System, width, height);
            }
        }

        private void GetTextures(Camera Camera, DynamicDecals System, int Width, int Height)
        {
            switch (replacement)
            {
                case 1:
                    //Get single combined buffer for everything
                    depthBuffer = RenderTexture.GetTemporary(Width, Height, 24, RenderTextureFormat.RGFloat);

                    //VR compatible, get eye version as required
                    if (VRCamera(Camera) && DynamicDecals.isSinglePassVR) depthEye = RenderTexture.GetTemporary(UnityEngine.XR.XRSettings.eyeTextureWidth, UnityEngine.XR.XRSettings.eyeTextureHeight, 24, RenderTextureFormat.RGFloat);
                    break;

                case 2:
                    //Get seperate buffers for everything
                    depthBuffer = RenderTexture.GetTemporary(Width, Height, 24, System.depthFormat);
                    normalBuffer = RenderTexture.GetTemporary(Width, Height, 0, System.normalFormat);
                    maskBuffer = RenderTexture.GetTemporary(Width, Height, 0, System.maskFormat);
                    break;

                case 3:
                    //Get seperate buffers for everything
                    if (Camera.actualRenderingPath == RenderingPath.DeferredShading) depthBuffer = RenderTexture.GetTemporary(Width, Height, 24, System.depthFormat);
                    normalBuffer = RenderTexture.GetTemporary(Width, Height, 24, System.normalFormat);
                    maskBuffer = RenderTexture.GetTemporary(Width, Height, 0, System.maskFormat);
                    break;

                case 4:
                    //Get seperate buffers for everything
                    if (Camera.actualRenderingPath == RenderingPath.DeferredShading) depthBuffer = RenderTexture.GetTemporary(Width, Height, 24, System.depthFormat);
                    normalBuffer = RenderTexture.GetTemporary(Width, Height, 24, System.normalFormat);
                    maskBuffer = RenderTexture.GetTemporary(Width, Height, 24, System.maskFormat);

                    //VR compatible, get eye version as required
                    if (VRCamera(Camera) && DynamicDecals.isSinglePassVR)
                    {
                        if (Camera.actualRenderingPath == RenderingPath.DeferredShading) depthEye = RenderTexture.GetTemporary(UnityEngine.XR.XRSettings.eyeTextureWidth, UnityEngine.XR.XRSettings.eyeTextureHeight, 24, System.depthFormat);
                        normalEye = RenderTexture.GetTemporary(UnityEngine.XR.XRSettings.eyeTextureWidth, UnityEngine.XR.XRSettings.eyeTextureHeight, 24, System.normalFormat);
                        maskEye = RenderTexture.GetTemporary(UnityEngine.XR.XRSettings.eyeTextureWidth, UnityEngine.XR.XRSettings.eyeTextureHeight, 24, System.maskFormat);
                    }
                    break;

                case 5:
                    //Only need mask buffer
                    maskBuffer = RenderTexture.GetTemporary(Width, Height, 24, System.maskFormat);
                    break;
            }
        }
        private void ReleaseTextures()
        {
            //Depth
            if (depthBuffer != null && depthBuffer.IsCreated())
            {
                RenderTexture.ReleaseTemporary(depthBuffer);
                depthBuffer = null;

                if (depthEye != null && depthEye.IsCreated())
                {
                    RenderTexture.ReleaseTemporary(depthEye);
                    depthEye = null;
                }
            }

            //Normal
            if (normalBuffer != null && normalBuffer.IsCreated())
            {
                RenderTexture.ReleaseTemporary(normalBuffer);
                normalBuffer = null;

                if (normalEye != null && normalEye.IsCreated())
                {
                    RenderTexture.ReleaseTemporary(normalEye);
                    normalEye = null;
                }
            }

            //Mask
            if (maskBuffer != null && maskBuffer.IsCreated())
            {
                RenderTexture.ReleaseTemporary(maskBuffer);
                maskBuffer = null;

                if (maskEye != null && maskEye.IsCreated())
                {
                    RenderTexture.ReleaseTemporary(maskEye);
                    maskEye = null;
                }
            }
        }

        //Custom shader replacement
        private void UpdateShaderReplacement(Camera Source, DynamicDecals System)
        {
            if (System.ShaderReplacement)
            {
                //Grab and setup replacement camera
                Camera Renderer = System.CustomCamera;
                SetupReplacementCamera(Source, Renderer);

                //Render into textures
                if (VRCamera(Source) && DynamicDecals.isSinglePassVR)
                {
                    //Left eye
                    if (Source.stereoTargetEye == StereoTargetEyeMask.Both || Source.stereoTargetEye == StereoTargetEyeMask.Left)
                    {
                        //Position
                        if (Source.transform.parent != null)
                        {
                            Renderer.transform.position = Source.transform.parent.TransformPoint(UnityEngine.XR.InputTracking.GetLocalPosition(UnityEngine.XR.XRNode.LeftEye));
                        }
                        else
                        {
                            Renderer.transform.position = UnityEngine.XR.InputTracking.GetLocalPosition(UnityEngine.XR.XRNode.LeftEye);
                        }

                        //Rotation
                        Renderer.transform.rotation = Source.transform.rotation * UnityEngine.XR.InputTracking.GetLocalRotation(UnityEngine.XR.XRNode.LeftEye);

                        //Projection matrix
                        Renderer.projectionMatrix = Source.GetStereoProjectionMatrix(Camera.StereoscopicEye.Left);

                        //View matrix
                        Renderer.worldToCameraMatrix = Source.worldToCameraMatrix;

                        //Render
                        RenderToTextures(Source, Renderer, System, depthEye, normalEye, maskEye);

                        //Blit into buffer
                        StereoBlit(Source, System, true);
                    }

                    //Right eye
                    if (Source.stereoTargetEye == StereoTargetEyeMask.Both || Source.stereoTargetEye == StereoTargetEyeMask.Right)
                    {
                        if (Source.transform.parent != null)
                        {
                            //Position
                            Renderer.transform.position = Source.transform.parent.TransformPoint(UnityEngine.XR.InputTracking.GetLocalPosition(UnityEngine.XR.XRNode.RightEye));

                            //View matrix
                            Matrix4x4 worldToCamera = Source.worldToCameraMatrix;
                            worldToCamera.m03 -= Source.stereoSeparation * Source.transform.parent.localScale.x;
                            Renderer.worldToCameraMatrix = worldToCamera;
                        }
                        else
                        {
                            //Position
                            Renderer.transform.position = UnityEngine.XR.InputTracking.GetLocalPosition(UnityEngine.XR.XRNode.RightEye);

                            //View matrix
                            Matrix4x4 worldToCamera = Source.worldToCameraMatrix;
                            worldToCamera.m03 -= Source.stereoSeparation;
                            Renderer.worldToCameraMatrix = worldToCamera;
                        }

                        //Rotation
                        Renderer.transform.rotation = Source.transform.rotation * UnityEngine.XR.InputTracking.GetLocalRotation(UnityEngine.XR.XRNode.RightEye);

                        //Projection matrix
                        Renderer.projectionMatrix = Source.GetStereoProjectionMatrix(Camera.StereoscopicEye.Right);

                        //Render
                        RenderToTextures(Source, Renderer, System, depthEye, normalEye, maskEye);

                        //Blit into buffer
                        StereoBlit(Source, System, false);
                    }
                }
                else RenderToTextures(Source, Renderer, System, depthBuffer, normalBuffer, maskBuffer);
            }
        }

        private void RenderToTextures(Camera Source, Camera Renderer, DynamicDecals System, RenderTexture depth, RenderTexture normal, RenderTexture mask)
        {
            switch (replacement)
            {
                case 5:
                    //Render to mask buffer
                    Renderer.targetTexture = mask;
                    DrawSplitPass(Source, Renderer, System, System.MaskShader, false);
                    break;

                case 4:
                    //Set culling layers
                    Renderer.cullingMask = Source.cullingMask;

                    //Render to depth buffer
                    if (Source.actualRenderingPath == RenderingPath.DeferredShading)
                    {
                        Renderer.targetTexture = depth;
                        DrawRegualarPass(Renderer, System.DepthShader);
                    }

                    //Render to normal buffer
                    Renderer.targetTexture = normal;
                    DrawRegualarPass(Renderer, System.NormalShader);

                    //Render to mask buffer
                    Renderer.targetTexture = mask;
                    DrawSplitPass(Source, Renderer, System, System.MaskShader);
                    break;

                case 3:

                    //Render to depth buffer
                    if (Source.actualRenderingPath == RenderingPath.DeferredShading)
                    {
                        Renderer.cullingMask = Source.cullingMask;
                        Renderer.targetTexture = depth;
                        DrawRegualarPass(Renderer, System.DepthShader);
                    }

                    //Render to normal and mask buffers at once
                    RenderBuffer[] doublePassBuffers = new RenderBuffer[] { normal.colorBuffer, mask.colorBuffer };
                    Renderer.SetTargetBuffers(doublePassBuffers, normal.depthBuffer);
                    DrawSplitPass(Source, Renderer, System, System.NormalMaskShader);
                    break;

                case 2:
                    //Render to depth, normal and mask buffers at once
                    RenderBuffer[] singlePassBuffers = new RenderBuffer[] { mask.colorBuffer, normal.colorBuffer, depth.colorBuffer };
                    Renderer.SetTargetBuffers(singlePassBuffers, depth.depthBuffer);
                    DrawSplitPass(Source, Renderer, System, System.DepthNormalMaskShader);
                    break;

                case 1:
                    //Render to packed buffer
                    Renderer.targetTexture = depth;
                    DrawSplitPass(Source, Renderer, System, System.DepthNormalMaskShader_Packed);
                    break;
            }

            //Tell camera to stop rendering to render texture/s
            Renderer.targetTexture = null;
        }
        private void DrawRegualarPass(Camera Renderer, Shader ReplacementShader)
        {
            //Clear before drawing
            Renderer.clearFlags = CameraClearFlags.SolidColor;
            Renderer.backgroundColor = Color.clear;

            //Render into temporary render texture
            Renderer.RenderWithShader(ReplacementShader, "RenderType");
        }
        private void DrawSplitPass(Camera Source, Camera Renderer, DynamicDecals System, Shader ReplacementShader, bool RenderInvalid = true)
        {
            //Grab mask passes
            // List<int> passes = System.Settings.Passes;

            //Clear on the first pass
            Renderer.clearFlags = CameraClearFlags.SolidColor;
            Renderer.backgroundColor = Color.clear;

            Renderer.cullingMask = Source.cullingMask;
            Renderer.RenderWithShader(ReplacementShader, "RenderType");
            //Only clear on first render
            Renderer.clearFlags = CameraClearFlags.Nothing;
        }

        private void StereoBlit(Camera Source, DynamicDecals System, bool Left)
        {
            //Blit from eye into buffer
            switch (replacement)
            {
                case 1:
                    //Packed
                    Graphics.Blit(depthEye, depthBuffer, (Left) ? System.StereoBlitLeft : System.StereoBlitRight);
                    break;

                case 4:
                    //Depth (deferred only)
                    if (Source.actualRenderingPath == RenderingPath.DeferredShading)
                    {
                        Material depthBlit = (Left) ? System.StereoDepthBlitLeft : System.StereoDepthBlitRight;

                        depthBlit.SetTexture("_DepthTex", depthEye);
                        Graphics.Blit(depthEye, depthBuffer, depthBlit);
                    }
                    //Normal
                    Graphics.Blit(normalEye, normalBuffer, (Left) ? System.StereoBlitLeft : System.StereoBlitRight);
                    //Mask
                    Graphics.Blit(maskEye, maskBuffer, (Left) ? System.StereoBlitLeft : System.StereoBlitRight);
                    break;
            }
        }

        private void SetupReplacementCamera(Camera Source, Camera Target)
        {
            Target.CopyFrom(Source);
            Target.renderingPath = RenderingPath.Forward;
            Target.depthTextureMode = DepthTextureMode.None;
            Target.useOcclusionCulling = false;
            Target.allowMSAA = false;
            Target.allowHDR = false;
            Target.rect = new Rect(0, 0, 1, 1);
        }
        private void SetupReplacementCameraExperimental(Camera Source, Camera Target)
        {
            //The idea behind this was to avoid association with VR cameras by avoiding the CopyFrom(VRCamera)
            //Breaks more than it fixes

            //Physical position
            Target.transform.position = Source.transform.position;
            Target.transform.rotation = Source.transform.rotation;

            //Fov and ranges
            if (!UnityEngine.XR.XRSettings.enabled) Target.fieldOfView = Source.fieldOfView;
            Target.nearClipPlane = Source.nearClipPlane;
            Target.farClipPlane = Source.farClipPlane;
            Target.rect = new Rect(0, 0, 1, 1);

            //Orthographic support
            Target.orthographic = Source.orthographic;
            Target.orthographicSize = Source.orthographicSize;

            //Matrix
            Target.ResetProjectionMatrix();
            Target.ResetWorldToCameraMatrix();

            //Force forward rendering and disable extravagences
            Target.renderingPath = RenderingPath.Forward;
            Target.depthTextureMode = DepthTextureMode.None;
            Target.useOcclusionCulling = false;
            Target.allowMSAA = false;
            Target.allowHDR = false;
            Target.eventMask = 0;
        }
    }
}